package com.ipan.kits.text;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 * XML、Map互转工具
 * 
 * 针对部分xml的格式是动态变化的，不是固定格式，这种不太适合使用XML与JavaBean的映射，所以，改为使用XML与Map的映射；
 * 
 * 注意：不支持带属性的XML，适用于键值对的XML；
 * 
 * @author iPan
 * @version 2019-03-21
 */
public class XmlMapUtil {
	
	private static final String ROOT_ELEMENT_NAME = "xml";
	private static final String ENCODING = "utf-8";
	private static final boolean IS_FORMAT = true;
	private static final boolean IS_CDATA = false; // true 说明不被XML解析器解析
	
	public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { // XXE防护
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // 主要是这个发挥作用
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);
        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
    
	/**
	 * Map转XML，支持Map可以多层转换；
	 */
	public static String mapToXml(String rootName, Map<String, ?> params, boolean isCDATA, boolean isFormat) {
		Document doc = null;
		String result = null;
		try {
			doc = newDocument();
			Element root = doc.createElement(rootName);
			doc.appendChild(root);
			iteratorXml(doc, root, params, isCDATA);
			result = null;
			TransformerFactory tf = TransformerFactory.newInstance();
		    Transformer transformer = tf.newTransformer();
		    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		    if(isFormat) {
		    	transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			}
		    StringWriter writer = null;
		    try {
				writer = new StringWriter();
				transformer.transform(new DOMSource(doc), new StreamResult(writer));
				result = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
			} catch (TransformerException e) {
				throw new RuntimeException(e);
			} finally {
				if (writer != null) {
					try {
						writer.close();
					} catch (IOException e) {
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return result;
	}
	public static String mapToXml(String rootName, Map<String, ?> params) {
		return mapToXml(rootName, params, IS_CDATA, IS_FORMAT);
	}
	public static String mapToXml(Map<String, ?> params) {
		return mapToXml(ROOT_ELEMENT_NAME, params, IS_CDATA, IS_FORMAT);
	}
	
	/**
	 * 循环遍历创建xml节点
	 */
	@SuppressWarnings("unchecked")
	public static void iteratorXml(Document doc, Element element, Map<String, ?> params, boolean isCDATA) {
		Set<String> set = params.keySet();
		for (Iterator<String> it = set.iterator(); it.hasNext();) {
			String key = (String) it.next();
			Object objVal = params.get(key);
			if (objVal instanceof Map) {
				Element filed = doc.createElement(key);
				element.appendChild(filed);
				iteratorXml(doc, filed, (Map<String, Object>) objVal, isCDATA);
			} else {
				String value = (objVal == null) ? "" : objVal.toString();
				Element filed = doc.createElement(key);
				if (isCDATA) {
					if (objVal instanceof Number) {
						filed.appendChild(doc.createTextNode(value));
					} else {
						filed.appendChild(doc.createCDATASection(value));
					}
				} else {
					filed.appendChild(doc.createTextNode(value));
				}
				element.appendChild(filed);
			}
		}
	}

	/**
	 * XML转Map
	 */
	public static Map<String, Object> xmlToMap(String xml) {
		return xmlToMap(xml, false);
	}
	public static Map<String, Object> xmlToMap(String xml, boolean hasRoot) {
		if(xml == null || xml.length() < 1) return null;
		Document doc = null;
		try {
			String encode = getEncoding(xml);
			InputStream sin = new ByteArrayInputStream(xml.getBytes(encode));
			doc = newDocumentBuilder().parse(sin);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (doc == null) {
			return null;
		}
		Map<String, Object> eleMap = new HashMap<String, Object>();
		Map<String, Object> retMap = eleMap;
		doc.getDocumentElement().normalize();
		NodeList nodeList = doc.getFirstChild().getChildNodes();
		nodeToMap(nodeList, eleMap);
		if (hasRoot) {
			String ename = doc.getFirstChild().getNodeName();
			retMap = new HashMap<String, Object>();
			retMap.put(ename, eleMap);
		}
		return retMap;
	}

	/***
	 * 递归遍历NodeList，放入Map；
	 */
	public static void nodeToMap(NodeList nodeList, Map<String, Object> outMap) {
		if (nodeList == null || nodeList.getLength() < 1) {
			return ;
		}
		for (int i=0, len=nodeList.getLength(); i < len; ++i) {
			Node node = nodeList.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				if (node.hasChildNodes() && node.getChildNodes().getLength() > 1) { // 存在子元素
					String name = node.getNodeName();
					Map<String, Object> innerMap = new HashMap<String, Object>();
					outMap.put(name, innerMap);
					nodeToMap(node.getChildNodes(), innerMap);
				} else {
					Element element = (Element) node;
					outMap.put(element.getNodeName(), element.getTextContent());
				}
			}
		}
	}
	
	private static String getEncoding(String text) {
        String result = null;
        String xml = text.trim();
        if (xml.startsWith("<?xml")) {
            int end = xml.indexOf("?>");
            String sub = xml.substring(0, end);
            StringTokenizer tokens = new StringTokenizer(sub, " =\"\'");
            while (tokens.hasMoreTokens()) {
                String token = tokens.nextToken();
                if ("encoding".equals(token)) {
                    if (tokens.hasMoreTokens()) {
                        result = tokens.nextToken();
                    }
                    break;
                }
            }
        }
        return (result != null && result.length() > 0) ? result : ENCODING;
    }
	
	/**
	 * 脱掉尖括号，防止与XML冲突；
	 * 同时清除了不见特殊控制字符；
	 */
	public static String clearXml(String xml) {
		if (StringUtils.isBlank(xml)) {
			return "";
		}
		xml = xml.replaceAll("[<>]", "");
		xml = StringUtils.replaceIgnoreCase(xml, "&lt;", "");
		xml = StringUtils.replaceIgnoreCase(xml, "&gt;", "");
		xml = MoreStringUtil.trimAsciiControl(xml);
		return xml;
	}
	
//	public static void main(String[] args) throws Exception {
//		Map<String, Object> data = new HashMap<String, Object>();
//		data.put("request", "getData");
//		Map<String, Object> map = new HashMap<String, Object>();
//		map.put("date", "2019-03-21");
//		map.put("name", "张三");
//		map.put("age", 11);
//		map.put("temp", null);
//		data.put("data", map);
//		
//		System.out.println(mapToXml(data));
//		System.out.println(mapToXml("root", data));
//		System.out.println(mapToXml("root", data, true, false));
//		
//		System.out.println(xmlToMap(mapToXml(data)));
//		System.out.println(xmlToMap(mapToXml(data), true));
//	}

}
